import { BoxGeometry } from '../../Source/Cesium.js';
import { Cartesian3 } from '../../Source/Cesium.js';
import { Color } from '../../Source/Cesium.js';
import { ColorGeometryInstanceAttribute } from '../../Source/Cesium.js';
import { destroyObject } from '../../Source/Cesium.js';
import { Ellipsoid } from '../../Source/Cesium.js';
import { GeometryInstance } from '../../Source/Cesium.js';
import { PolygonGeometry } from '../../Source/Cesium.js';
import { Rectangle } from '../../Source/Cesium.js';
import { RectangleGeometry } from '../../Source/Cesium.js';
import { ShowGeometryInstanceAttribute } from '../../Source/Cesium.js';
import { Transforms } from '../../Source/Cesium.js';
import { Pass } from '../../Source/Cesium.js';
import { RenderState } from '../../Source/Cesium.js';
import { ClassificationPrimitive } from '../../Source/Cesium.js';
import { ClassificationType } from '../../Source/Cesium.js';
import { InvertClassification } from '../../Source/Cesium.js';
import { MaterialAppearance } from '../../Source/Cesium.js';
import { PerInstanceColorAppearance } from '../../Source/Cesium.js';
import { Primitive } from '../../Source/Cesium.js';
import { StencilConstants } from '../../Source/Cesium.js';
import createScene from '../createScene.js';
import pollToPromise from '../pollToPromise.js';

describe('Scene/ClassificationPrimitive', function() {

    var scene;

    var ellipsoid;
    var rectangle;

    var depthColor;
    var boxColor;

    var boxInstance;
    var primitive;
    var globePrimitive;
    var tilesetPrimitive;
    var reusableGlobePrimitive;
    var reusableTilesetPrimitive;

    function createPrimitive(rectangle, pass) {
        var renderState;
        if (pass === Pass.CESIUM_3D_TILE) {
            renderState = RenderState.fromCache({
                stencilTest : StencilConstants.setCesium3DTileBit(),
                stencilMask : StencilConstants.CESIUM_3D_TILE_MASK,
                depthTest : {
                    enabled : true
                }
            });
        }
        var depthColorAttribute = ColorGeometryInstanceAttribute.fromColor(new Color(0.0, 0.0, 1.0, 1.0));
        depthColor = depthColorAttribute.value;
        return new Primitive({
            geometryInstances : new GeometryInstance({
                geometry : new RectangleGeometry({
                    ellipsoid : Ellipsoid.WGS84,
                    rectangle : rectangle
                }),
                id : 'depth rectangle',
                attributes : {
                    color : depthColorAttribute
                }
            }),
            appearance : new PerInstanceColorAppearance({
                translucent : false,
                flat : true,
                renderState : renderState
            }),
            asynchronous : false
        });
    }

    function MockPrimitive(primitive, pass) {
        this._primitive = primitive;
        this._pass = pass;
        this.show = true;
    }

    MockPrimitive.prototype.update = function(frameState) {
        if (!this.show) {
            return;
        }

        var commandList = frameState.commandList;
        var startLength = commandList.length;
        this._primitive.update(frameState);

        for (var i = startLength; i < commandList.length; ++i) {
            var command = commandList[i];
            command.pass = this._pass;
        }
    };

    MockPrimitive.prototype.isDestroyed = function() {
        return false;
    };

    MockPrimitive.prototype.destroy = function() {
        return destroyObject(this);
    };

    beforeAll(function() {
        scene = createScene();
        scene.postProcessStages.fxaa.enabled = false;

        ellipsoid = Ellipsoid.WGS84;

        rectangle = Rectangle.fromDegrees(-75.0, 25.0, -70.0, 30.0);
        reusableGlobePrimitive = createPrimitive(rectangle, Pass.GLOBE);
        reusableTilesetPrimitive = createPrimitive(rectangle, Pass.CESIUM_3D_TILE);
    });

    afterAll(function() {
        reusableGlobePrimitive.destroy();
        reusableTilesetPrimitive.destroy();
        scene.destroyForSpecs();
    });

    beforeEach(function() {
        scene.morphTo3D(0);

        // wrap rectangle primitive so it gets executed during the globe pass and 3D Tiles pass to lay down depth
        globePrimitive = new MockPrimitive(reusableGlobePrimitive, Pass.GLOBE);
        tilesetPrimitive = new MockPrimitive(reusableTilesetPrimitive, Pass.CESIUM_3D_TILE);

        var center = Rectangle.center(rectangle);
        var origin = ellipsoid.cartographicToCartesian(center);
        var modelMatrix = Transforms.eastNorthUpToFixedFrame(origin);

        var dimensions = new Cartesian3(1000000.0, 1000000.0, 1000000.0);

        var boxColorAttribute = ColorGeometryInstanceAttribute.fromColor(new Color(1.0, 1.0, 0.0, 1.0));
        boxColor = boxColorAttribute.value;
        boxInstance = new GeometryInstance({
            geometry : BoxGeometry.fromDimensions({
                dimensions : dimensions
            }),
            modelMatrix : modelMatrix,
            id : 'box',
            attributes : {
                color : boxColorAttribute
            }
        });
    });

    afterEach(function() {
        scene.primitives.removeAll();
        primitive = primitive && !primitive.isDestroyed() && primitive.destroy();
        globePrimitive = globePrimitive && !globePrimitive.isDestroyed() && globePrimitive.destroy();
        tilesetPrimitive = tilesetPrimitive && !tilesetPrimitive.isDestroyed() && tilesetPrimitive.destroy();
    });

    it('default constructs', function() {
        primitive = new ClassificationPrimitive();
        expect(primitive.geometryInstances).not.toBeDefined();
        expect(primitive.show).toEqual(true);
        expect(primitive.vertexCacheOptimize).toEqual(false);
        expect(primitive.interleave).toEqual(false);
        expect(primitive.compressVertices).toEqual(true);
        expect(primitive.releaseGeometryInstances).toEqual(true);
        expect(primitive.allowPicking).toEqual(true);
        expect(primitive.asynchronous).toEqual(true);
        expect(primitive.debugShowBoundingVolume).toEqual(false);
        expect(primitive.debugShowShadowVolume).toEqual(false);
    });

    it('constructs with options', function() {
        var geometryInstances = [];

        primitive = new ClassificationPrimitive({
            geometryInstances : geometryInstances,
            show : false,
            vertexCacheOptimize : true,
            interleave : true,
            compressVertices : false,
            releaseGeometryInstances : false,
            allowPicking : false,
            asynchronous : false,
            debugShowBoundingVolume : true,
            debugShowShadowVolume : true
        });

        expect(primitive.geometryInstances).toEqual(geometryInstances);
        expect(primitive.show).toEqual(false);
        expect(primitive.vertexCacheOptimize).toEqual(true);
        expect(primitive.interleave).toEqual(true);
        expect(primitive.compressVertices).toEqual(false);
        expect(primitive.releaseGeometryInstances).toEqual(false);
        expect(primitive.allowPicking).toEqual(false);
        expect(primitive.asynchronous).toEqual(false);
        expect(primitive.debugShowBoundingVolume).toEqual(true);
        expect(primitive.debugShowShadowVolume).toEqual(true);
    });

    it('releases geometry instances when releaseGeometryInstances is true', function() {
        if (!ClassificationPrimitive.isSupported(scene)) {
            return;
        }

        primitive = new ClassificationPrimitive({
            geometryInstances : boxInstance,
            releaseGeometryInstances : true,
            asynchronous : false
        });

        expect(primitive.geometryInstances).toBeDefined();
        scene.primitives.add(primitive);
        scene.renderForSpecs();
        expect(primitive.geometryInstances).not.toBeDefined();
    });

    it('does not release geometry instances when releaseGeometryInstances is false', function() {
        if (!ClassificationPrimitive.isSupported(scene)) {
            return;
        }

        primitive = new ClassificationPrimitive({
            geometryInstances : boxInstance,
            releaseGeometryInstances : false,
            asynchronous : false
        });

        expect(primitive.geometryInstances).toBeDefined();
        scene.primitives.add(primitive);
        scene.renderForSpecs();
        expect(primitive.geometryInstances).toBeDefined();
    });

    it('adds afterRender promise to frame state', function() {
        if (!ClassificationPrimitive.isSupported(scene)) {
            return;
        }

        primitive = new ClassificationPrimitive({
            geometryInstances : boxInstance,
            releaseGeometryInstances : false,
            asynchronous : false
        });

        scene.primitives.add(primitive);
        scene.renderForSpecs();

        return primitive.readyPromise.then(function(param) {
            expect(param.ready).toBe(true);
        });
    });

    it('does not render when geometryInstances is undefined', function() {
        if (!ClassificationPrimitive.isSupported(scene)) {
            return;
        }

        primitive = new ClassificationPrimitive({
            geometryInstances : undefined,
            appearance : new PerInstanceColorAppearance(),
            asynchronous : false
        });

        scene.primitives.add(primitive);
        scene.renderForSpecs();
        expect(scene.frameState.commandList.length).toEqual(0);
    });

    it('does not render when show is false', function() {
        if (!ClassificationPrimitive.isSupported(scene)) {
            return;
        }

        primitive = new ClassificationPrimitive({
            geometryInstances : boxInstance,
            asynchronous : false
        });

        scene.primitives.add(primitive);
        scene.renderForSpecs();
        expect(scene.frameState.commandList.length).toBeGreaterThan(0);

        primitive.show = false;
        scene.renderForSpecs();
        expect(scene.frameState.commandList.length).toEqual(0);
    });

    it('becomes ready when show is false', function() {
        if (!ClassificationPrimitive.isSupported(scene)) {
            return;
        }

        primitive = scene.primitives.add(new ClassificationPrimitive({
            geometryInstances : boxInstance
        }));
        primitive.show = false;

        var ready = false;
        primitive.readyPromise.then(function() {
            ready = true;
        });

        return pollToPromise(function() {
            scene.renderForSpecs();
            return ready;
        }).then(function() {
            expect(ready).toEqual(true);
        });
    });

    it('does not render other than for the color or pick pass', function() {
        if (!ClassificationPrimitive.isSupported(scene)) {
            return;
        }

        primitive = new ClassificationPrimitive({
            geometryInstances : boxInstance,
            asynchronous : false
        });

        var frameState = scene.frameState;
        frameState.passes.render = false;
        frameState.passes.pick = false;

        primitive.update(frameState);
        expect(frameState.commandList.length).toEqual(0);
    });

    function expectRender(color) {
        expect(scene).toRender(color);
    }

    function expectRenderBlank() {
        expect(scene).toRenderAndCall(function(rgba) {
            expect(rgba).not.toEqual([0, 0, 0, 255]);
            expect(rgba[0]).toEqual(0);
        });
    }

    function verifyClassificationPrimitiveRender(primitive, color) {
        scene.camera.setView({ destination : rectangle });

        scene.primitives.add(globePrimitive);
        scene.primitives.add(tilesetPrimitive);

        expectRenderBlank();

        scene.primitives.add(primitive);

        primitive.classificationType = ClassificationType.BOTH;
        globePrimitive.show = false;
        tilesetPrimitive.show = true;
        expectRender(color);
        globePrimitive.show = true;
        tilesetPrimitive.show = false;
        expectRender(color);

        primitive.classificationType = ClassificationType.CESIUM_3D_TILE;
        globePrimitive.show = false;
        tilesetPrimitive.show = true;
        expectRender(color);
        globePrimitive.show = true;
        tilesetPrimitive.show = false;
        expectRenderBlank();

        primitive.classificationType = ClassificationType.TERRAIN;
        globePrimitive.show = false;
        tilesetPrimitive.show = true;
        expectRenderBlank();
        globePrimitive.show = true;
        tilesetPrimitive.show = false;
        expectRender(color);

        globePrimitive.show = true;
        tilesetPrimitive.show = true;
    }

    it('renders in 3D', function() {
        if (!ClassificationPrimitive.isSupported(scene)) {
            return;
        }

        primitive = new ClassificationPrimitive({
            geometryInstances : boxInstance,
            asynchronous : false
        });

        verifyClassificationPrimitiveRender(primitive, boxColor);
    });

    // Rendering in 2D/CV is broken:
    // https://github.com/CesiumGS/cesium/issues/6308
    xit('renders in Columbus view when scene3DOnly is false', function() {
        if (!ClassificationPrimitive.isSupported(scene)) {
            return;
        }

        primitive = new ClassificationPrimitive({
            geometryInstances : boxInstance,
            asynchronous : false
        });

        scene.morphToColumbusView(0);
        verifyClassificationPrimitiveRender(primitive, boxColor);
    });

    xit('renders in 2D when scene3DOnly is false', function() {
        if (!ClassificationPrimitive.isSupported(scene)) {
            return;
        }

        primitive = new ClassificationPrimitive({
            geometryInstances : boxInstance,
            asynchronous : false
        });

        scene.morphTo2D(0);
        verifyClassificationPrimitiveRender(primitive, boxColor);
    });

    it('renders batched instances', function() {
        if (!ClassificationPrimitive.isSupported(scene)) {
            return;
        }

        var neCarto = Rectangle.northeast(rectangle);
        var nwCarto = Rectangle.northwest(rectangle);

        var ne = ellipsoid.cartographicToCartesian(neCarto);
        var nw = ellipsoid.cartographicToCartesian(nwCarto);

        var direction = Cartesian3.subtract(ne, nw, new Cartesian3());
        var distance = Cartesian3.magnitude(direction) * 0.25;
        Cartesian3.normalize(direction, direction);
        Cartesian3.multiplyByScalar(direction, distance, direction);

        var center = Rectangle.center(rectangle);
        var origin = ellipsoid.cartographicToCartesian(center);

        var origin1 = Cartesian3.add(origin, direction, new Cartesian3());
        var modelMatrix = Transforms.eastNorthUpToFixedFrame(origin1);

        var dimensions = new Cartesian3(500000.0, 1000000.0, 1000000.0);

        var boxColorAttribute = ColorGeometryInstanceAttribute.fromColor(new Color(0.0, 1.0, 1.0, 1.0));
        var boxInstance1 = new GeometryInstance({
            geometry : BoxGeometry.fromDimensions({
                dimensions : dimensions
            }),
            modelMatrix : modelMatrix,
            id : 'box1',
            attributes : {
                color : boxColorAttribute
            }
        });

        Cartesian3.negate(direction, direction);
        var origin2 = Cartesian3.add(origin, direction, new Cartesian3());
        modelMatrix = Transforms.eastNorthUpToFixedFrame(origin2);

        var boxInstance2 = new GeometryInstance({
            geometry : BoxGeometry.fromDimensions({
                dimensions : dimensions
            }),
            modelMatrix : modelMatrix,
            id : 'box2',
            attributes : {
                color : boxColorAttribute
            }
        });

        primitive = new ClassificationPrimitive({
            geometryInstances : [boxInstance1, boxInstance2],
            asynchronous : false
        });
        verifyClassificationPrimitiveRender(primitive, boxColorAttribute.value);
    });

    it('renders with invert classification and an opaque color', function() {
        if (!ClassificationPrimitive.isSupported(scene)) {
            return;
        }

        scene.invertClassification = true;
        scene.invertClassificationColor = new Color(0.25, 0.25, 0.25, 1.0);

        boxInstance.attributes.show = new ShowGeometryInstanceAttribute(true);

        primitive = new ClassificationPrimitive({
            geometryInstances : boxInstance,
            asynchronous : false
        });

        scene.camera.setView({ destination : rectangle });

        var invertedColor = new Array(4);
        invertedColor[0] = Color.floatToByte(Color.byteToFloat(depthColor[0]) * scene.invertClassificationColor.red);
        invertedColor[1] = Color.floatToByte(Color.byteToFloat(depthColor[1]) * scene.invertClassificationColor.green);
        invertedColor[2] = Color.floatToByte(Color.byteToFloat(depthColor[2]) * scene.invertClassificationColor.blue);
        invertedColor[3] = 255;

        scene.primitives.add(tilesetPrimitive);
        expect(scene).toRender(invertedColor);

        scene.primitives.add(primitive);
        expect(scene).toRender(boxColor);

        primitive.getGeometryInstanceAttributes('box').show = [0];
        expect(scene).toRender(depthColor);

        scene.invertClassification = false;
    });

    it('renders with invert classification and a translucent color', function() {
        if (!ClassificationPrimitive.isSupported(scene)) {
            return;
        }

        if (!InvertClassification.isTranslucencySupported(scene.context)) {
            return;
        }

        scene.invertClassification = true;
        scene.invertClassificationColor = new Color(0.25, 0.25, 0.25, 0.25);

        boxInstance.attributes.show = new ShowGeometryInstanceAttribute(true);

        primitive = new ClassificationPrimitive({
            geometryInstances : boxInstance,
            asynchronous : false
        });

        scene.camera.setView({ destination : rectangle });

        var invertedColor = new Array(4);
        invertedColor[0] = Color.floatToByte(Color.byteToFloat(depthColor[0]) * scene.invertClassificationColor.red  * scene.invertClassificationColor.alpha);
        invertedColor[1] = Color.floatToByte(Color.byteToFloat(depthColor[1]) * scene.invertClassificationColor.green * scene.invertClassificationColor.alpha);
        invertedColor[2] = Color.floatToByte(Color.byteToFloat(depthColor[2]) * scene.invertClassificationColor.blue * scene.invertClassificationColor.alpha);
        invertedColor[3] = 255;

        scene.primitives.add(tilesetPrimitive);
        expect(scene).toRender(invertedColor);

        scene.primitives.add(primitive);
        expect(scene).toRender(boxColor);

        primitive.getGeometryInstanceAttributes('box').show = [0];
        expect(scene).toRender(depthColor);

        scene.invertClassification = false;
    });

    it('renders bounding volume with debugShowBoundingVolume', function() {
        if (!ClassificationPrimitive.isSupported(scene)) {
            return;
        }

        primitive = new ClassificationPrimitive({
            geometryInstances : boxInstance,
            asynchronous : false,
            debugShowBoundingVolume : true
        });

        scene.primitives.add(primitive);
        scene.camera.setView({ destination : rectangle });
        expect(scene).toRenderAndCall(function(rgba) {
            expect(rgba[1]).toBeGreaterThanOrEqualTo(0);
            expect(rgba[1]).toBeGreaterThanOrEqualTo(0);
            expect(rgba[2]).toBeGreaterThanOrEqualTo(0);
            expect(rgba[3]).toEqual(255);
        });
    });

    it('renders shadow volume with debugShowShadowVolume', function() {
        if (!ClassificationPrimitive.isSupported(scene)) {
            return;
        }

        primitive = new ClassificationPrimitive({
            geometryInstances : boxInstance,
            asynchronous : false,
            debugShowShadowVolume : true
        });

        scene.primitives.add(primitive);
        scene.camera.setView({ destination : rectangle });
        expect(scene).toRenderAndCall(function(rgba) {
            expect(rgba[1]).toBeGreaterThanOrEqualTo(0);
            expect(rgba[1]).toBeGreaterThanOrEqualTo(0);
            expect(rgba[2]).toBeGreaterThanOrEqualTo(0);
            expect(rgba[3]).toEqual(255);
        });
    });

    it('get per instance attributes', function() {
        if (!ClassificationPrimitive.isSupported(scene)) {
            return;
        }

        primitive = new ClassificationPrimitive({
            geometryInstances : boxInstance,
            asynchronous : false
        });

        verifyClassificationPrimitiveRender(primitive, boxColor);

        var attributes = primitive.getGeometryInstanceAttributes('box');
        expect(attributes.color).toBeDefined();
    });

    it('modify color instance attribute', function() {
        if (!ClassificationPrimitive.isSupported(scene)) {
            return;
        }

        primitive = new ClassificationPrimitive({
            geometryInstances : boxInstance,
            asynchronous : false
        });

        verifyClassificationPrimitiveRender(primitive, boxColor);

        scene.primitives.destroyPrimitives = false;
        scene.primitives.removeAll();
        scene.primitives.destroyPrimitives = true;

        scene.primitives.destroyPrimitives = false;
        scene.primitives.removeAll();
        scene.primitives.destroyPrimitives = true;

        var newColor = [255, 255, 255, 255];
        var attributes = primitive.getGeometryInstanceAttributes('box');
        expect(attributes.color).toBeDefined();
        attributes.color = newColor;

        verifyClassificationPrimitiveRender(primitive, newColor);
    });

    it('modify show instance attribute', function() {
        if (!ClassificationPrimitive.isSupported(scene)) {
            return;
        }

        boxInstance.attributes.show = new ShowGeometryInstanceAttribute(true);

        primitive = new ClassificationPrimitive({
            geometryInstances : boxInstance,
            asynchronous : false
        });

        verifyClassificationPrimitiveRender(primitive, boxColor);

        scene.primitives.destroyPrimitives = false;
        scene.primitives.removeAll();
        scene.primitives.destroyPrimitives = true;

        scene.primitives.destroyPrimitives = false;
        scene.primitives.removeAll();
        scene.primitives.destroyPrimitives = true;

        var attributes = primitive.getGeometryInstanceAttributes('box');
        expect(attributes.show).toBeDefined();
        attributes.show = [0];

        verifyClassificationPrimitiveRender(primitive, depthColor);
    });

    it('get bounding sphere from per instance attribute', function() {
        if (!ClassificationPrimitive.isSupported(scene)) {
            return;
        }

        primitive = new ClassificationPrimitive({
            geometryInstances : boxInstance,
            asynchronous : false
        });

        verifyClassificationPrimitiveRender(primitive, boxColor);

        var attributes = primitive.getGeometryInstanceAttributes('box');
        expect(attributes.boundingSphere).toBeDefined();
    });

    it('getGeometryInstanceAttributes returns same object each time', function() {
        if (!ClassificationPrimitive.isSupported(scene)) {
            return;
        }

        primitive = new ClassificationPrimitive({
            geometryInstances : boxInstance,
            asynchronous : false
        });

        verifyClassificationPrimitiveRender(primitive, boxColor);

        var attributes = primitive.getGeometryInstanceAttributes('box');
        var attributes2 = primitive.getGeometryInstanceAttributes('box');
        expect(attributes).toBe(attributes2);
    });

    it('picking', function() {
        if (!ClassificationPrimitive.isSupported(scene)) {
            return;
        }

        primitive = new ClassificationPrimitive({
            geometryInstances : boxInstance,
            asynchronous : false
        });

        verifyClassificationPrimitiveRender(primitive, boxColor);

        expect(scene).toPickAndCall(function(result) {
            expect(result.id).toEqual('box');
        });
    });

    it('drill picking', function() {
        if (!ClassificationPrimitive.isSupported(scene)) {
            return;
        }

        primitive = new ClassificationPrimitive({
            geometryInstances : boxInstance,
            asynchronous : false
        });

        verifyClassificationPrimitiveRender(primitive, boxColor);

        expect(scene).toDrillPickAndCall(function(pickedObjects) {
            expect(pickedObjects.length).toEqual(3);
            expect(pickedObjects[0].primitive).toEqual(primitive);
            expect(pickedObjects[1].primitive).toEqual(globePrimitive._primitive);
            expect(pickedObjects[2].primitive).toEqual(tilesetPrimitive._primitive);
        });
    });

    it('does not pick when allowPicking is false', function() {
        if (!ClassificationPrimitive.isSupported(scene)) {
            return;
        }

        primitive = new ClassificationPrimitive({
            geometryInstances : boxInstance,
            allowPicking : false,
            asynchronous : false
        });

        verifyClassificationPrimitiveRender(primitive, boxColor);

        expect(scene).notToPick();
    });

    it('internally invalid asynchronous geometry resolves promise and sets ready', function() {
        if (!ClassificationPrimitive.isSupported(scene)) {
            return;
        }

        primitive = new ClassificationPrimitive({
            geometryInstances : new GeometryInstance({
                geometry : PolygonGeometry.fromPositions({
                    positions : []
                }),
                attributes: {
                    color: ColorGeometryInstanceAttribute.fromColor(Color.RED)
                }
            }),
            compressVertices : false
        });

        scene.primitives.add(primitive);

        return pollToPromise(function() {
            scene.renderForSpecs();
            return primitive.ready;
        }).then(function() {
            return primitive.readyPromise.then(function(arg) {
                expect(arg).toBe(primitive);
                expect(primitive.ready).toBe(true);
            });
        });
    });

    it('internally invalid synchronous geometry resolves promise and sets ready', function() {
        if (!ClassificationPrimitive.isSupported(scene)) {
            return;
        }

        primitive = new ClassificationPrimitive({
            geometryInstances : new GeometryInstance({
                geometry : PolygonGeometry.fromPositions({
                    positions : []
                }),
                attributes: {
                    color: ColorGeometryInstanceAttribute.fromColor(Color.RED)
                }
            }),
            asynchronous : false,
            compressVertices : false
        });

        scene.primitives.add(primitive);
        scene.renderForSpecs();
        return primitive.readyPromise.then(function(arg) {
            expect(arg).toBe(primitive);
            expect(primitive.ready).toBe(true);
        });
    });

    it('update throws when batched instance colors are different and no culling attributes are provided', function() {
        if (!ClassificationPrimitive.isSupported(scene)) {
            return;
        }

        var neCarto = Rectangle.northeast(rectangle);
        var nwCarto = Rectangle.northwest(rectangle);

        var ne = ellipsoid.cartographicToCartesian(neCarto);
        var nw = ellipsoid.cartographicToCartesian(nwCarto);

        var direction = Cartesian3.subtract(ne, nw, new Cartesian3());
        var distance = Cartesian3.magnitude(direction) * 0.25;
        Cartesian3.normalize(direction, direction);
        Cartesian3.multiplyByScalar(direction, distance, direction);

        var center = Rectangle.center(rectangle);
        var origin = ellipsoid.cartographicToCartesian(center);

        var origin1 = Cartesian3.add(origin, direction, new Cartesian3());
        var modelMatrix = Transforms.eastNorthUpToFixedFrame(origin1);

        var dimensions = new Cartesian3(500000.0, 1000000.0, 1000000.0);

        var boxColorAttribute = ColorGeometryInstanceAttribute.fromColor(new Color(0.0, 1.0, 1.0, 1.0));
        var boxInstance1 = new GeometryInstance({
            geometry : BoxGeometry.fromDimensions({
                dimensions : dimensions
            }),
            modelMatrix : modelMatrix,
            id : 'box1',
            attributes : {
                color : ColorGeometryInstanceAttribute.fromColor(new Color(0.0, 1.0, 1.0, 1.0))
            }
        });

        Cartesian3.negate(direction, direction);
        var origin2 = Cartesian3.add(origin, direction, new Cartesian3());
        modelMatrix = Transforms.eastNorthUpToFixedFrame(origin2);

        var boxInstance2 = new GeometryInstance({
            geometry : BoxGeometry.fromDimensions({
                dimensions : dimensions
            }),
            modelMatrix : modelMatrix,
            id : 'box2',
            attributes : {
                color : ColorGeometryInstanceAttribute.fromColor(new Color(1.0, 0.0, 1.0, 1.0))
            }
        });

        primitive = new ClassificationPrimitive({
            geometryInstances : [boxInstance1, boxInstance2],
            asynchronous : false
        });

        expect(function() {
            verifyClassificationPrimitiveRender(primitive, boxColorAttribute.value);
        }).toThrowDeveloperError();
    });

    it('update throws when one batched instance color is undefined', function() {
        if (!ClassificationPrimitive.isSupported(scene)) {
            return;
        }

        var neCarto = Rectangle.northeast(rectangle);
        var nwCarto = Rectangle.northwest(rectangle);

        var ne = ellipsoid.cartographicToCartesian(neCarto);
        var nw = ellipsoid.cartographicToCartesian(nwCarto);

        var direction = Cartesian3.subtract(ne, nw, new Cartesian3());
        var distance = Cartesian3.magnitude(direction) * 0.25;
        Cartesian3.normalize(direction, direction);
        Cartesian3.multiplyByScalar(direction, distance, direction);

        var center = Rectangle.center(rectangle);
        var origin = ellipsoid.cartographicToCartesian(center);

        var origin1 = Cartesian3.add(origin, direction, new Cartesian3());
        var modelMatrix = Transforms.eastNorthUpToFixedFrame(origin1);

        var dimensions = new Cartesian3(500000.0, 1000000.0, 1000000.0);

        var boxColorAttribute = ColorGeometryInstanceAttribute.fromColor(new Color(0.0, 1.0, 1.0, 1.0));
        var boxInstance1 = new GeometryInstance({
            geometry : BoxGeometry.fromDimensions({
                dimensions : dimensions
            }),
            modelMatrix : modelMatrix,
            id : 'box1',
            attributes : {
                color : ColorGeometryInstanceAttribute.fromColor(new Color(0.0, 1.0, 1.0, 1.0))
            }
        });

        Cartesian3.negate(direction, direction);
        var origin2 = Cartesian3.add(origin, direction, new Cartesian3());
        modelMatrix = Transforms.eastNorthUpToFixedFrame(origin2);

        var boxInstance2 = new GeometryInstance({
            geometry : BoxGeometry.fromDimensions({
                dimensions : dimensions
            }),
            modelMatrix : modelMatrix,
            id : 'box2'
        });

        primitive = new ClassificationPrimitive({
            geometryInstances : [boxInstance1, boxInstance2],
            asynchronous : false
        });

        expect(function() {
            verifyClassificationPrimitiveRender(primitive, boxColorAttribute.value);
        }).toThrowDeveloperError();
    });

    it('update throws when no batched instance colors are given for a PerInstanceColorAppearance', function() {
        if (!ClassificationPrimitive.isSupported(scene)) {
            return;
        }

        var neCarto = Rectangle.northeast(rectangle);
        var nwCarto = Rectangle.northwest(rectangle);

        var ne = ellipsoid.cartographicToCartesian(neCarto);
        var nw = ellipsoid.cartographicToCartesian(nwCarto);

        var direction = Cartesian3.subtract(ne, nw, new Cartesian3());
        var distance = Cartesian3.magnitude(direction) * 0.25;
        Cartesian3.normalize(direction, direction);
        Cartesian3.multiplyByScalar(direction, distance, direction);

        var center = Rectangle.center(rectangle);
        var origin = ellipsoid.cartographicToCartesian(center);

        var origin1 = Cartesian3.add(origin, direction, new Cartesian3());
        var modelMatrix = Transforms.eastNorthUpToFixedFrame(origin1);

        var dimensions = new Cartesian3(500000.0, 1000000.0, 1000000.0);

        var boxInstance1 = new GeometryInstance({
            geometry : BoxGeometry.fromDimensions({
                dimensions : dimensions
            }),
            modelMatrix : modelMatrix,
            id : 'box1'
        });

        primitive = new ClassificationPrimitive({
            geometryInstances : [boxInstance1],
            asynchronous : false,
            appearance : new PerInstanceColorAppearance()
        });

        var boxColorAttribute = ColorGeometryInstanceAttribute.fromColor(new Color(0.0, 1.0, 1.0, 1.0));

        expect(function() {
            verifyClassificationPrimitiveRender(primitive, boxColorAttribute.value);
        }).toThrowDeveloperError();
    });

    it('update throws when the given Appearance is incompatible with the geometry instance attributes', function() {
        if (!ClassificationPrimitive.isSupported(scene)) {
            return;
        }

        primitive = new ClassificationPrimitive({
            geometryInstances : [boxInstance],
            asynchronous : false,
            appearance : new MaterialAppearance()
        });

        expect(function() {
            verifyClassificationPrimitiveRender(primitive, [255, 255, 255, 255]);
        }).toThrowDeveloperError();
    });

    it('update throws when an incompatible Appearance is set', function() {
        if (!ClassificationPrimitive.isSupported(scene)) {
            return;
        }

        primitive = new ClassificationPrimitive({
            geometryInstances : [boxInstance],
            asynchronous : false,
            appearance : new PerInstanceColorAppearance()
        });

        scene.camera.setView({ destination : rectangle });
        scene.primitives.add(globePrimitive);
        expect(scene).toRenderAndCall(function(rgba) {
            expect(rgba).not.toEqual([0, 0, 0, 255]);
            expect(rgba[0]).toEqual(0);
        });

        scene.primitives.add(primitive);
        expect(scene).toRender([255, 255, 0, 255]);

        // become incompatible
        primitive.appearance = new MaterialAppearance();

        expect(function() {
            expect(scene).toRender([255, 255, 255, 255]);
        }).toThrowDeveloperError();
    });

    it('setting per instance attribute throws when value is undefined', function() {
        if (!ClassificationPrimitive.isSupported(scene)) {
            return;
        }

        primitive = new ClassificationPrimitive({
            geometryInstances : boxInstance,
            asynchronous : false
        });

        verifyClassificationPrimitiveRender(primitive, boxColor);

        var attributes = primitive.getGeometryInstanceAttributes('box');

        expect(function() {
            attributes.color = undefined;
        }).toThrowDeveloperError();
    });

    it('can disable picking when asynchronous', function() {
        if (!ClassificationPrimitive.isSupported(scene)) {
            return;
        }

        primitive = new ClassificationPrimitive({
            geometryInstances : boxInstance,
            asynchronous : true,
            allowPicking : false
        });

        scene.primitives.add(primitive);

        return pollToPromise(function() {
            scene.renderForSpecs();
            return primitive.ready;
        }).then(function() {
            var attributes = primitive.getGeometryInstanceAttributes('box');
            expect(function() {
                attributes.color = undefined;
            }).toThrowDeveloperError();
        });
    });

    it('getGeometryInstanceAttributes throws without id', function() {
        if (!ClassificationPrimitive.isSupported(scene)) {
            return;
        }

        primitive = new ClassificationPrimitive({
            geometryInstances : boxInstance,
            asynchronous : false
        });

        verifyClassificationPrimitiveRender(primitive, boxColor);

        expect(function() {
            primitive.getGeometryInstanceAttributes();
        }).toThrowDeveloperError();
    });

    it('getGeometryInstanceAttributes throws if update was not called', function() {
        primitive = new ClassificationPrimitive({
            geometryInstances : boxInstance,
            asynchronous : false
        });

        expect(function() {
            primitive.getGeometryInstanceAttributes('box');
        }).toThrowDeveloperError();
    });

    it('getGeometryInstanceAttributes returns undefined if id does not exist', function() {
        if (!ClassificationPrimitive.isSupported(scene)) {
            return;
        }

        primitive = new ClassificationPrimitive({
            geometryInstances : boxInstance,
            asynchronous : false
        });

        verifyClassificationPrimitiveRender(primitive, boxColor);

        expect(primitive.getGeometryInstanceAttributes('unknown')).not.toBeDefined();
    });

    it('isDestroyed', function() {
        primitive = new ClassificationPrimitive();
        expect(primitive.isDestroyed()).toEqual(false);
        primitive.destroy();
        expect(primitive.isDestroyed()).toEqual(true);
    });

    it('renders when using asynchronous pipeline', function() {
        if (!ClassificationPrimitive.isSupported(scene)) {
            return;
        }

        primitive = new ClassificationPrimitive({
            geometryInstances : boxInstance
        });

        scene.primitives.add(primitive);

        return pollToPromise(function() {
            scene.renderForSpecs();
            return primitive.ready;
        }).then(function() {
            // verifyClassificationPrimitiveRender adds the primitive, so remove it to avoid being added twice.
            scene.primitives.destroyPrimitives = false;
            scene.primitives.removeAll();
            scene.primitives.destroyPrimitives = true;

            verifyClassificationPrimitiveRender(primitive, boxColor);
        });
    });

    it('destroy before asynchronous pipeline is complete', function() {
        if (!ClassificationPrimitive.isSupported(scene)) {
            return;
        }

        primitive = new ClassificationPrimitive({
            geometryInstances : boxInstance
        });

        scene.primitives.add(primitive);

        scene.renderForSpecs();
        primitive.destroy();
        expect(primitive.isDestroyed()).toEqual(true);

        // The primitive has already been destroyed, so remove it from the scene so it doesn't get destroyed again.
        scene.primitives.destroyPrimitives = false;
        scene.primitives.removeAll();
        scene.primitives.destroyPrimitives = true;
    });

}, 'WebGL');
